﻿// HSS.Interlink.Web.BaseUploadHandler.cs
// ----------------------------------------------------------------------------
// Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.html)
// ----------------------------------------------------------------------------
// HighSpeed-Solutions, LLC
// Copyright (c) 2001-2012
// ----------------------------------------------------------------------------
// File:       BaseUploadHandler.cs
// Author:     HSS\gbanta
// Created:    07/16/2011
// Modified:   12/19/2011
// ----------------------------------------------------------------------------
namespace HSS.Interlink.Web
{
	#region Using Directives
	using System;
	using System.Configuration;
	using System.IO;
	using System.Net;
	using System.Security.Cryptography;
	using System.Web;
	using System.Web.Configuration;
	#endregion

	#region BaseUploadHandler
	/// <summary>
	/// BaseUploadHandler is the base implementation of a File upload
	/// processor and handles the Protocol portion of a upload request.
	/// Implementors of this class should override the abstract processing
	/// methods.
	/// </summary>
	public abstract class BaseUploadHandler : BaseHandler, IUploadHandler
	{
		#region Constants
		private const string SUCCESS_PREFIX = "__success";
		#endregion

		#region Fields
		/// <summary>
		/// Array of hex values 0-F
		/// </summary>
		static int[] hexValue = new int[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
		#endregion

		#region Constructors
		/// <summary>
		/// Constructor
		/// </summary>
		public BaseUploadHandler()
		{

		}
		#endregion

		#region Properties
		/// <summary>
		/// Gets any Metadata associated with the current request
		/// </summary>
		public string Metadata
		{
			get;
			internal set;
		}
		/// <summary>
		/// Gets or sets the number of maximum retry attempts. Default: 3 attempts
		/// </summary>
		public int MaxRetryAttempts
		{
			get { return _maxRetryAttempts; }
			set { _maxRetryAttempts = value; }
		} int _maxRetryAttempts = 3;
		/// <summary>
		/// Gets the unique cache key for storing the count of retry attempts.
		/// </summary>
		public string RetryKey
		{
			get
			{
				if (string.IsNullOrEmpty(_retryKey))
					_retryKey = HttpUtility.UrlEncode(this.Query.JobId + this.FileName + "_retries");
				return _retryKey;
			}
		} string _retryKey;
		/// <summary>
		/// Gets the current number of retry attempts.
		/// </summary>
		public int RetryAttempts
		{
			get
			{
				var retries = 0;
				try
				{
					var objRetries = this.Context.Cache[this.RetryKey];
					if (null == objRetries)
						retries = 0;
					else
						retries = (int)objRetries;
				}
				catch
				{
					retries = 0;
				}
				return retries;
			}
			private set
			{
				this.Context.Cache[this.RetryKey] = value;
			}
		}
		#endregion

		#region Methods

		/// <summary>
		/// Processes the actual upload request. This method is used by HSS.Interlink and not meant to be called from user code.
		/// </summary>
		/// <param name="context">The current HttpContext</param>
		/// <param name="fileQuery">The FileQuery</param>
		void IUploadHandler.Upload(HttpContext context, FileQuery fileQuery)
		{
			var response = Response.Success;
			var jobFile = string.Empty;

			try
			{
				jobFile = this.CommonInit(context, fileQuery);

				switch (context.Request.HttpMethod.ToUpperInvariant())
				{
					case "POST":
						#region POST commands
						switch (this.Query.Command)
						{
							case FileQueryCommand.Prepare:
								#region Prepare
								UploadTempCache.Current.Add(jobFile, context.Request);
								context.Response.Clear();
								context.Response.Write(this.GetMaxRequestLength());
								return;
								#endregion

							case FileQueryCommand.Append:
								#region Append

								byte[] buffer;
								if (this.PreProcessAppendRequest(jobFile, ref response, out buffer))
									response = this.AppendToFile(buffer); // Implementation
								context.Response.Clear();
								if (response == Response.AppendFileRetry)
								{
									var retries = this.RetryAttempts++;
									if (retries >= this.MaxRetryAttempts)
										response = Response.FatalError;
								}
								if (response != Response.Success)
									context.Response.Write(Responses.GetResponse(response));
								buffer = null;

								return;

								#endregion

							default:
								#region Default is failure
								context.Request.InputStream.Close();
								context.Response.Clear();
								context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
								return;
								#endregion
						}
						#endregion

					case "GET":
						#region GET commands
						switch (this.Query.Command)
						{
							case FileQueryCommand.CheckFileExists:
								#region CheckFileExists
								var fileExists = false;
								if (this.PrepareJob(jobFile, ref response))
									fileExists = this.CheckFileExists();
								context.Response.Clear();
								if (response == Response.Success)
								{
									if (fileExists)
										response = Response.FileAlreadyExists;
									else
										response = Response.FileDoesNotExist;
								}
								context.Response.Write(Responses.GetResponse(response));
								return;
								#endregion

							case FileQueryCommand.NewFile:
								#region CreateNewFile
								if (this.PrepareJob(jobFile, ref response))
									this.CreateNewFile();
								context.Response.Clear();
								if (response != Response.Success)
									context.Response.Write(Responses.GetResponse(response));
								return;
								#endregion

							case FileQueryCommand.CancelUpload:
								#region Cancel
								if (this.PrepareJob(jobFile, ref response))
								{
									try
									{
										this.CancelUpload();
									}
									catch { }
								}
								else
								{
									try
									{
										var ex = new Exception(Responses.GetResponse(response));
										OnError(ref ex);
									}
									catch { }
								}
								this.Reset();
								context.Response.Clear();
								return;
								#endregion

							case FileQueryCommand.UploadComplete:
								#region Complete
								var result = "";
								if (this.PrepareJob(jobFile, ref response))
								{
									try
									{
										result = this.UploadComplete();
										this.Reset();
									}
									catch
									{
										response = Response.FatalError;
									}
								}
								context.Response.Clear();
								if (response != Response.Success)
									context.Response.Write(Responses.GetResponse(response));
								else if (!string.IsNullOrEmpty(result))
									context.Response.Write(HttpUtility.HtmlEncode(SUCCESS_PREFIX + result));
								return;
								#endregion

							default:
								#region Default is failure
								context.Request.InputStream.Close();
								context.Response.Clear();
								context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
								return;
								#endregion
						}
						#endregion

					default:
						#region No other VERBS supported
						context.Request.InputStream.Close();
						context.Response.Clear();
						context.Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
						return;
						#endregion
				}
			}
			catch (Exception ex)
			{
				#region Handle Exceptions
				try
				{
					OnError(ref ex);
				}
				catch { }
				try
				{
					context.Response.Clear();
					context.Response.Write(ex.Message);
				}
				catch { }
				#endregion
			}

		}

		#region Private
		private void Reset()
		{
			try { this.Context.Cache.Remove(this.RetryKey); }
			catch { }
		}
		private bool PrepareJob(string jobFile, ref Response response)
		{
			if (!this.SetMetadata(jobFile))
			{
				response = Response.FatalError;
				return false;
			}
			if (!this.IsAuthorizedInternal())
			{
				response = Response.UnauthorizedRequest;
				return false;
			}
			return true;
		}
		private bool SetMetadata(string fileName, bool closeInputStream = true)
		{
			var filePath = "";
			if (!UploadTempCache.Current.TryGetFilePath(fileName, out filePath))
			{
				this.Context.Request.InputStream.Close();
				this.Context.Response.Clear();
				this.Context.Response.StatusCode = 500;
				return false;
			}
			this.Metadata = File.ReadAllText(filePath);
			if (closeInputStream)
				this.Context.Request.InputStream.Close();
			return true;
		}
		private bool PreProcessAppendRequest(string jobFile, ref Response response, out byte[] buffer)
		{
			buffer = null;

			if (!this.SetMetadata(jobFile, false))
			{
				response = Response.FatalError;
				return false;
			}
			if (!this.IsAuthorizedInternal())
			{
				response = Response.UnauthorizedRequest;
				return false;
			}
			var inputStream = this.Context.Request.InputStream;
			var fileLen = inputStream.Length;
			if (fileLen == 0)
			{
				response = Response.FatalError;
				return false;
			}
			var hashLen = 32L; // (256BIT/8)
			var bufferLen = fileLen - hashLen;
			buffer = new byte[bufferLen];

			var hashBuffer = new byte[hashLen];
			inputStream.Read(buffer, 0, buffer.Length);
			inputStream.Read(hashBuffer, 0, hashBuffer.Length);
			inputStream.Close();

			var actualHash = new SHA256Managed().ComputeHash(buffer);

			return CompareHash(hashBuffer, actualHash);
		}
		private string GetMaxRequestLength()
		{
			string maxLen = "4096";
			try
			{
				HttpRuntimeSection runtime = ConfigurationManager.GetSection(Properties.Resources.Config_HttpRuntimeConfig_Key) as HttpRuntimeSection;
				maxLen = runtime.MaxRequestLength.ToString();
			}
			catch { }
			return Properties.Resources.Cookie_MaxChunkSize + maxLen;
		}
		#endregion

		#region Abstract (API)
		/// <summary>
		/// Determines if the requested file already exists in the destination
		/// </summary>
		/// <returns>true if the file exists; otherwise false</returns>
		public abstract bool CheckFileExists();
		/// <summary>
		/// This command is called to create a new file, but does not contain
		/// any contents of the file (see AppendToFile).
		/// </summary>
		public abstract void CreateNewFile();
		/// <summary>
		/// Receives a chunk of data to append to an existing file.
		/// </summary>
		/// <param name="buffer">The bytes available for writing.</param>
		/// <returns>true if the process was successful; otherwise false.</returns>
		public abstract Response AppendToFile(byte[] buffer);
		/// <summary>
		/// Cancel the current Upload request.
		/// </summary>
		public abstract void CancelUpload();
		/// <summary>
		/// Complete the current Upload request.
		/// </summary>
		/// <returns>An arbitrary string to return as a result of the file being successfully uploaded.</returns>
		public abstract string UploadComplete();
		/// <summary>
		/// Called when an error is encountered.
		/// </summary>
		/// <param name="ex">The exception that was encountered.</param>
		public abstract void OnError(ref Exception ex);
		#endregion

		#endregion
	}
	#endregion
}